import os, sys
import threading
import jsonpickle

from Bases import ChildGroup
from Bases import OSCBaseObject
#from DeviceSystem.DeviceSystem import DeviceSystem
#from DeviceSystem import Groups
import DeviceSystem
from modifiers.EffectsSystem import EffectsSystem
from modifiers.snapshot import Stack, Snapshot
from modifiers.playback import Playback

Groups = DeviceSystem.Groups


class MainController(OSCBaseObject):
    _saved_class_name = 'MainController'
    _saved_child_objects = ['Groups', 'CueStacks', 'Submasters']
    #saved_child_classes = ['Stack']
    def __init__(self, **kwargs):
        self.comm = kwargs.get('comm')
        self.parse_args = kwargs.get('parse_args')
        self.osc_address = 'Main'
        self.osc_parent_node = self.comm.osc_io.root_node
        self.osc_child_nodes = {}
        super(MainController, self).__init__(**kwargs)
        self.loading_from_osc = False
        self._osc_dumps_pending = {}
        self.add_osc_handler(callbacks={'request-dump':self.on_osc_request_dump, 
                                        'dump-response':self.on_osc_dump_response, 
                                        'child-added':self.on_osc_child_added})
                                        
        self.register_signal('new_group', 'remove_group', 'group_index_changed', 
                             'new_cuestack', 'new_submaster', 'playback_added', 'child_added')
        self.signal_dict_map = {'new_group':'Groups', 'new_cuestack':'CueStacks', 
                                'new_submaster':'Submasters'}
        for sig in self.signal_dict_map.iterkeys():
            self.connect(sig, self.on_signals_to_osc)
            
        ds_kwargs = {'MainController':self}
        ds_kwargs.update(self.add_osc_child(address='Devices'))
        self.DeviceSystem = DeviceSystem.DeviceSystem(**ds_kwargs)
        self.DeviceSystem.connect('new_device', self.on_ds_new_device)
        self.EffectsSystem = EffectsSystem(DeviceSystem=self.DeviceSystem)
        
        for key in ['CueStacks', 'Groups', 'Submasters', 'Playbacks']:
            cg = ChildGroup(name=key, osc_parent_node=self.osc_node)
            setattr(self, key, cg)

        self.load_globals()
        
        f = self.parse_args['options'].filename
        if f:
            t = threading.Timer(10.0, self.load_file, kwargs={'filename':f})
            t.start()
            #self.load_file(filename=f)
            
        #self.comm.connect('state_changed', self.on_comm_state)
        self.comm.osc_io.connect('new_master', self.on_osc_new_master)
                
    def request_osc_updates(self, **kwargs):
        self._osc_dumps_pending.clear()
        self.osc_node.send_message(address='request-dump', value=list(self.saved_child_objects), to_master=True)
        self.DeviceSystem.request_osc_updates()
            
    def on_osc_new_master(self, **kwargs):
        if not kwargs.get('master_is_local'):
            self.request_osc_updates()
            
    def add_to_selection(self, *args):
        self.Selection.add_devices(args)
        
    def remove_from_selection(self, *args):
        for arg in args:
            self.Selection.del_member(arg.id)
        
    def add_group(self, **kwargs):
        '''
        :Parameters:
            id : 
            name : 
        '''
        gid = kwargs.get('id')
        if gid is not None and gid in self.Groups:
            return self.Groups[gid]
        kwargs['MainController'] = self
        kwargs.setdefault('osc_address', kwargs.get('name'))
        kwargs.setdefault('root_category', self.DeviceSystem.root_category.subcategories['device'])
        group = self.Groups.add_child(Groups.DeviceGroup.DeviceGroup, **kwargs)
        group.connect('member_obj_request', self.on_group_member_obj_request)
        self.emit('new_group', id=group.id, group=group)
        self.emit('child_added')
        return group
        
    def del_group(self, **kwargs):
        gid = kwargs.get('id')
        group = self.Groups.get(gid)
        if group:
            group.unlink()
            self.Groups.del_child(group)
            #del self.Groups[gid]
            #if group.index in self.groups_indexed:
            #    del self.groups_indexed[group.index]
            ## TODO: make this tell OSC clients to delete the group also
            self.emit('remove_group', id=gid, group=group)
    
    def add_CueStack(self, **kwargs):
        '''
        :Parameters:
            id :
            name :
        '''
        kwargs.update({'MainController':self})
        kwargs.setdefault('osc_address', kwargs.get('name'))
        stack = self.CueStacks.add_child(Stack, **kwargs)
        self.emit('new_cuestack', id=stack.id, stack=stack)
        self.emit('child_added')
        return stack
        
    def del_CueStack(self, **kwargs):
        csid = kwargs.get('id')
        cs = self.CueStacks.get(csid)
        if cs:
            cs.unlink()
            self.CueStacks.del_child(cs)
            
        
    def store_to_CueStack(self, **kwargs):
        '''
        :Parameters:
            stack : stack object
            stack_id : stack object id
            cue_index : int, cue number, defaults to stack's current cue
            source : str or list, what to store ('all', 'selected', [list, of, objects])
        '''
        stack = kwargs.get('stack')
        stack_id = kwargs.get('stack_id')
        if not stack and stack_id is not None:
            stack = self.CueStacks[stack_id]
        index = kwargs.get('index', stack.current_index)
        source = kwargs.get('source', 'selected')
        if index:
            if type(source) == list or type(source) == tuple:
                obj_list = source
            elif source == 'selected':
                obj_list = self.Selection.members.values()
            elif source == 'all':
                obj_list = self.AllDevices.members.values()
            else:
                obj_list = []
            objects = dict(zip([obj.id for obj in obj_list], obj_list))
            stack.store_snapshot(index=index, objects=objects)
            return True
        return False
        
    def add_playback(self, **kwargs):
        '''
        :Parameters:
            stack : stack object
            stack_id : stack object id
            name : 
        '''
        stack = kwargs.get('stack')
        stack_id = kwargs.get('stack_id')
        if not stack and stack_id is not None:
            stack = self.CueStacks[stack_id]
        if stack.playback is not None:
            return stack.playback
        pb_kwargs = kwargs.copy()
        pb_kwargs.update(dict(stack=stack, MainController=self))
        pb_kwargs.setdefault('name', stack.name)
        pb_kwargs.setdefault('osc_address', pb_kwargs.get('name'))
        pb = self.Playbacks.add_child(Playback, **pb_kwargs)
        self.emit('playback_added', playback=pb, id=pb.id)
        return pb
    
    def on_ds_new_device(self, **kwargs):
        id = kwargs.get('id')
        if kwargs.get('base') == 'PatchedDevice':
            obj = self.DeviceSystem.PatchedDevices[id]
#            self.AllDevices.membership_lock = False
#            self.AllDevices.add_devices(obj)
#            self.AllDevices.membership_lock = True
            for group in self.Groups.itervalues():
                group.request_member_obj()
            #print 'mc AllDevices:', self.AllDevices.members
            
    def on_group_member_obj_request(self, **kwargs):
        id = kwargs.get('id')
        group = kwargs.get('group')
        d = {}
        m = self.DeviceSystem.PatchedDevices.get(id)
        if not m:
            m = self.Groups.get(id)
        if m:
            d.update({id:m})
            #print 'adding:', id, m
        #else:
        #    print 'id not found:', id
            
        group.add_member(**d)
        
    def _clean_groups(self):
        for group in self.Groups.itervalues():
            group.request_member_obj()
            group.clean_members()
            
    def _clean_cuestacks(self):
        for cs in self.CueStacks.itervalues():
            cs.reload_member_obj()
        
    def on_osc_request_dump(self, **kwargs):
        if self.osc_node.oscMaster:
            values = kwargs.get('values')
            client = kwargs.get('client')
            #print 'main...client = ', client
            if len(values):
                for i, key in enumerate(values):
                    s = self.to_json(saved_child_objects=[key], indent=None)
                    self.osc_node.send_message(address='dump-response', value=[i, s], client=client)
            else:
                s = self.to_json(indent=None)
                self.osc_node.send_message(address='dump-response', value=[0, s], client=client)
            self.osc_node.send_message(address='dump-response', value='dump-complete', client=client)
        
    def on_osc_dump_response(self, **kwargs):
        values = kwargs.get('values')
        if values[0] == 'dump-complete':
            self.loading_from_osc = True
            keys = self._osc_dumps_pending.copy().keys()
            keys.sort()
            for key in keys:
                self.from_json(self._osc_dumps_pending[key])
                del self._osc_dumps_pending[key]
            self.loading_from_osc = False
            self._clean_groups()
            self._clean_cuestacks()
            #print 'mc dump-complete. keys=', self._osc_dumps_pending.keys()
            return
        #print 'mc adding dump ', values[0]
        self._osc_dumps_pending[values[0]] = values[1]
    
    def on_signals_to_osc(self, **kwargs):
        name = kwargs.get('signal_name')
        id = kwargs.get('id')
        key = self.signal_dict_map.get(name)
        if key and not self.loading_from_osc:
            obj = getattr(self, key).get(id)
            if obj:
                s = obj.to_json(indent=None)
                self.osc_node.send_message(address='child-added', value=[key, s])
        
    def on_osc_child_added(self, **kwargs):
        values = kwargs.get('values')
        key = values[0]
        d = jsonpickle.decode(values[1])
        s = jsonpickle.encode({'saved_children':{key:{d['attrs']['id']:d}}})
        self.from_json(s, saved_child_objects=[key])
        
    def load_file(self, **kwargs):
        filename = kwargs.get('filename')
        string = kwargs.get('string', '')
        if filename is not None and os.path.exists(filename):
            file = open(filename, 'r')
            for line in file:
                string += line
            file.close()
        if string != '':
            d = jsonpickle.decode(string)
            for key in ['DeviceSystem', 'EffectsSystem']:
                if key in d:
                    getattr(self, key).from_json(string=jsonpickle.encode(d[key]))
            self.from_json(string)
            self._clean_groups()
            self._clean_cuestacks()
        self.emit('child_added')
                    
    def save_file(self, **kwargs):
        filename = kwargs.get('filename')
        if filename:
            d = {}
            for key in ['DeviceSystem', 'EffectsSystem']:
                d.update({key:getattr(self, key)._get_saved_attr()})
            file = open(filename, 'w')
            file.write(self.to_json(d, **kwargs))
            file.close()
            self.save_globals()
    
    def load_globals(self, **kwargs):
        kwargs.setdefault('filename', 'Globals.json')
        filename = kwargs.get('filename')
        string = kwargs.get('string', '')
        if filename is not None and os.path.exists(filename):
            file = open(filename, 'r')
            for line in file:
                string += line
            file.close()
        if string != '':
            d = jsonpickle.decode(string)
            for key in ['DeviceSystem', 'EffectsSystem']:
                if key in d:
                    getattr(self, key).load_globals(string=jsonpickle.encode(d[key]))
            self._load_saved_attr(d, saved_child_objects=[])
        
    def save_globals(self, **kwargs):
        filename = kwargs.get('filename', 'Globals.json')
        d = {}
        for key in ['DeviceSystem', 'EffectsSystem']:
            d.update({key:getattr(self, key).save_globals()})
        file = open(filename, 'w')
        file.write(self.to_json(d, saved_child_objects=[]))
        file.close()
        
    def on_app_exit(self, *args, **kwargs):
        def do_exit():
            print 'exiting'
            print threading.enumerate()
            print [t._name for t in threading.enumerate() if hasattr(t, '_name')]
            sys.exit(0)
        print 'MainController preparing shutdown'
        self.comm.do_disconnect()
        t1 = threading.Timer(1.0, self.comm.on_app_exit)
        #print 'comm_app_exit:', t1.name
        t1.start()
        t1.join()
        t2 = threading.Timer(1.0, do_exit)
        #print 'doexit:', t2.name
        t2.start()
        t2.join()
        
    
    def _deserialize_child(self, d):
        if type(d) != dict:
            print d
            return
        if 'saved_class_name' not in d:
            print 'no class name', d
            return
        if d['saved_class_name'] == 'DeviceGroup' and d['attrs']['id'] not in self.Groups:
            #g_kwargs = self.add_osc_child(address=d['attrs']['name'])
            g_kwargs = dict(deserialize=d, osc_address=d['attrs']['osc_address'], 
                            #osc_parent_node=self.osc_child_nodes['Groups'], 
                            signals_to_connect={'member_obj_request':self.on_group_member_obj_request}, 
                            root_category=self.DeviceSystem.root_category.subcategories['device'], 
                            MainController=self)
            g = self.Groups.add_child(Groups.DeviceGroup.DeviceGroup, **g_kwargs)
            #g.connect('member_obj_request', self.on_group_member_obj_request)
            return g
        elif d['saved_class_name'] == 'Stack' and d['attrs']['id'] not in self.CueStacks:
            s_kwargs = dict(MainController=self, deserialize=d, 
                            osc_address=d['attrs']['osc_address'])
            return self.CueStacks.add_child(Stack, **s_kwargs)
        
    def _get_saved_attr(self, **kwargs):
        d = super(MainController, self)._get_saved_attr(**kwargs)
        for key in ['AllDevices', 'Selection']:
            id = '_'.join(['MainController', key])
            if id in d['saved_children'].get('Groups', {}):
                del d['saved_children']['Groups'][id]
        return d